17.3 启动
在为对象分配堆内存后,mallocgc函数会检查垃圾回收触发条件,并依照相关状态启动或参与辅助回收。
malloc.go
func mallocgc(size uintptr,typ*_type,flags uint32)unsafe.Pointer{ …
// 直接分配黑色对象 if gcphase== _GCmarktermination||gcBlackenPromptly{ systemstack(func() { gcmarknewobject_m(uintptr(x),size) }) }
// 检查垃圾回收触发条件 if shouldhelpgc&&shouldtriggergc() { // 启动并发垃圾回收 startGC(gcBackgroundMode,false) }else if gcBlackenEnabled!=0{ // 辅助参与回收任务 gcAssistAlloc(size,shouldhelpgc) }else if shouldhelpgc&&bggc.working!=0{ // 让出资源 gp:=getg() if gp!=gp.m.g0&&gp.m.locks0&&gp.m.preemptoff "" { Gosched() } } }
func shouldtriggergc()bool{ return memstats.heap_live>=memstats.next_gc&&atomicloaduint(&bggc.working) ==0 }
heap_live是活跃对象总量,不包括那些尚未被清理的白色对象。
垃圾回收默认以全并发模式运行,但可以用环境变量或参数禁用并发标记和并发清理。GC goroutine一直循环,直到符合触发条件时被唤醒。
mgc.go
func startGC(mode int,forceTrigger bool) { // 判断GODEBUG环境变量 //1: 禁用并发标记 //2: 禁用并发标记和并发清理 if debug.gcstoptheworld==1{ mode=gcForceMode }else if debug.gcstoptheworld==2{ mode=gcForceBlockMode }
// 同步阻塞模式 if mode!=gcBackgroundMode{ gc(mode) return }
// 检查触发条件 if!(forceTrigger||shouldtriggergc()) { return }
// 全局变量bggc保存GC状态 // 创建或唤醒GC goroutine if!bggc.started{ bggc.working=1 bggc.started=true go backgroundgc() }else if bggc.working==0{ bggc.working=1
// 唤醒
ready(bggc.g,0)
} }
var bggc struct{ g *g//GC goroutine working uint // 是否正处于工作状态 started bool // 是否已创建 }
func backgroundgc() { bggc.g=getg() for{ gc(gcBackgroundMode) bggc.working=0
// 休眠,等待再次被唤醒
goparkunlock(&bggc.lock, "Concurrent GC wait",traceEvGoBlock,1)
} }
经过种种手段的优化调整,在整个回收周期,STW被缩短到有限的几个片段,这让程序实时响应有了很大改善。
新GC的表现虽说不上惊艳,但足以让人相当惊喜。它代表了Go不断进化,以及开发团队追求卓越的精神,这让我对其前景更为看好。不过,当前版本有很多过渡痕迹,甚至代码和文档有对不上的地方。这种情形曾出现在1.3里,或许下一个版本才是最佳选择。
是否完全去掉STW?是否能优化写屏障的性能?Go还有很多问题尚待解决。
并发模式(Background Mode)垃圾回收过程示意图:
---------------------+--------------------------------------------------- | OFF+ ------⇒ 准备MarkWorker/P,使其休眠待命 | stop | B:1 BE:1]SCAN+ | start | + ------⇒ 并发扫描,将灰色对象放入队列 | 对白色对象的引用修改被写屏障捕获 | Malloc分配白色对象 | MarkWorker被唤醒,开始标记任务 | MARK+ | + ------⇒ 等待第一轮标记结束 | 第一轮处理的是并发扫描捕获的灰色对象,不包括新分配白色对象 | + ------⇒ 重新扫描DATA、BSS区域 | 扫描新分配白色对象 | + ------⇒ 等待第二轮标记结束 | stop | [BE:0] + | MARK TERMINATION+ | + ------⇒STW冻结,完成最终标记 | [WB:0]OFF+ | + ------⇒ 并发清理 | start | STW:StopTheWorld WB:WriteBarrierEnabled BE:BlackenEnabled
整个过程被封装在有些庞大的gc函数里。
mgc.go
func gc(mode int) { // 清理掉意外遗留的span for gosweepone() != ^uintptr(0) { sweep.nbgsweep++ }
// 创建MarkWorker(休眠状态) if mode==gcBackgroundMode{ gcBgMarkStartWorkers() }
//STW:STOP systemstack(stopTheWorldWithSema)
// 确保在进入扫描状态前,环境已清理干净 systemstack(finishsweep_m)
// 处理sync.Pool clearpools()
// 重置全局状态变量work gcResetMarkState()
// ---OFF(STW:STOP) -----------------------------------------------
// 并发标记模式 if mode==gcBackgroundMode{ // 控制器 gcController.startCycle()
systemstack(func() {
// 启用写屏障
setGCPhase(_GCscan)
// 初始化相关状态和信号
gcBgMarkPrepare()
// 允许黑色对象标记
atomicstore(&gcBlackenEnabled,1)
//STW:START
startTheWorldWithSema()
// ---SCAN(STW:START) ------------------------------
// 并发扫描
gcscan_m()
setGCPhase(_GCmark)
})
// ---MARK(STW:START) -------------------------------------
// 等待MarkWorker发回第一轮任务结束信号
work.bgMark1.clear()
work.bgMark1.wait()
// 第二轮扫描,目标新增白色对象和剩余区段
systemstack(func() {
//DATA、BSS保存全局变量
markroot(nil, _RootData)
markroot(nil, _RootBss)
gcBlackenPromptly=true
forEachP(func(_p_ *p) {
_p_.gcw.dispose()
})
})
// 等待MarkWorker发回第二轮任务结束信号
work.bgMark2.clear()
work.bgMark2.wait()
//STW:STOP
systemstack(stopTheWorldWithSema)
// 将所有P.gcw上交全局队列
gcFlushGCWork()
gcController.endCycle()
}else{ // 阻塞模式(mode!=gcBackgroundMode) gcResetGState() }
// ---MARK TERMINATION(STW:STOP) ----------------------------------
// 禁用黑色标记操作(MarkWorker停止工作) atomicstore(&gcBlackenEnabled,0) gcBlackenPromptly=false setGCPhase(_GCmarktermination)
// 完成最终标记工作 // 如果是阻塞模式,因为没有前期的扫描和标记操作,那么此处完成全部标记 systemstack(func() { gcMark(startTime) })
// ---OFF(STW:STOP) -----------------------------------------------
systemstack(func() { // 关闭写屏障 setGCPhase(_GCoff)
// 开启清理操作(并发或阻塞)
gcSweep(mode)
})
// 全部工作完成 //STW:START systemstack(startTheWorldWithSema) }